import {afterAll, beforeAll, describe, expect, it} from "vitest"
import waitPort from "wait-port"
import {blitzLaunchApp, findPort, killApp, runBlitzCommand} from "../../utils/next-test-utils"
import webdriver from "../../utils/next-webdriver"

import fetch from "node-fetch"
import {fromBase64} from "b64-lite"

let app: any
let appPort: number
const HEADER_CSRF = "anti-csrf"
const COOKIE_PUBLIC_DATA_TOKEN = "auth-tests-cookie-prefix_sPublicDataToken"
const COOKIE_SESSION_TOKEN = "auth-tests-cookie-prefix_sSessionToken"
const COOKIE_ANONYMOUS_SESSION_TOKEN = "auth-tests-cookie-prefix_sAnonymousSessionToken"
const COOKIE_REFRESH_TOKEN = "auth-tests-cookie-prefix_sIdRefreshToken"
const HEADER_PUBLIC_DATA_TOKEN = "public-data-token"

function readCookie(cookieHeader, name) {
  const setPos = cookieHeader.search(new RegExp("\\b" + name + "="))
  const stopPos = cookieHeader.indexOf(";", setPos)
  let res
  if (!~setPos) return undefined
  res = decodeURIComponent(
    cookieHeader.substring(setPos, ~stopPos ? stopPos : undefined).split("=")[1],
  )
  return res.charAt(0) === "{" ? JSON.parse(res) : res
}

const runTests = (mode?: string) => {
  describe("Tests", () => {
    describe("unauthenticated", () => {
      it(
        "should render error for protected query - client",
        async () => {
          await waitPort({port: appPort})
          const browser = await webdriver(appPort, "/authenticated-client")
          let errorMsg = await browser.elementById(`error`).text()
          expect(errorMsg).toMatch(/Error: You are not authenticated/)
          if (browser) await browser.close()
        },
        5000 * 60 * 2,
      )

      it(
        "should render error for protected query - server component",
        async () => {
          await waitPort({port: appPort})
          const browser = await webdriver(appPort, "/authenticated-server")
          let errorMsg = await browser.elementById(`error`).text()
          expect(errorMsg).toMatch(/Error: You are not authenticated/)
          if (browser) await browser.close()
        },
        5000 * 60 * 2,
      )

      it(
        "should render result for open query",
        async () => {
          const res = await fetch(`http://localhost:${appPort}/api/noauth`, {
            method: "POST",
            headers: {"Content-Type": "application/json; charset=utf-8"},
          })
          expect(res.status).toBe(200)
        },
        5000 * 60 * 2,
      )

      it("sets correct cookie", async () => {
        const res = await fetch(`http://localhost:${appPort}/api/noauth`, {
          method: "POST",
          headers: {"Content-Type": "application/json; charset=utf-8"},
        })
        const cookieHeader = res.headers.get("Set-Cookie")
        const cookie = (name) => readCookie(cookieHeader, name)

        expect(res.status).toBe(200)
        expect(res.headers.get(HEADER_CSRF)).not.toBe(undefined)
        expect(cookie(COOKIE_ANONYMOUS_SESSION_TOKEN)).not.toBeUndefined()
        expect(cookie(COOKIE_SESSION_TOKEN)).toBe("")
        expect(cookie(COOKIE_REFRESH_TOKEN)).toBeUndefined()

        expect(res.headers.get(HEADER_PUBLIC_DATA_TOKEN)).toBe("updated")
        expect(cookie(COOKIE_PUBLIC_DATA_TOKEN)).not.toBe(undefined)

        const publicDataStr = fromBase64(cookie(COOKIE_PUBLIC_DATA_TOKEN))
        const publicData = JSON.parse(publicDataStr)
        expect(publicData.userId).toBe(null)
      })
    })

    describe("RPC get query data", () => {
      it(
        "should work",
        async () => {
          const browser = await webdriver(appPort, "/react-query")

          await browser.refresh()

          browser.waitForElementByCss("#button", 0)
          await browser.elementByCss("#button").click()

          browser.waitForElementByCss("#new-data", 0)
          const newText = await browser.elementByCss("#new-data").text()
          expect(newText).toMatch(/basic-result/)

          if (browser) await browser.close()
        },
        5000 * 60 * 2,
      )
    })

    describe("authenticated", () => {
      it("should login successfully", async () => {
        const res = await fetch(
          `http://localhost:${appPort}/api/signin?email=test@test.com&password=abcd1234`,
          {
            method: "POST",
            headers: {"Content-Type": "application/json; charset=utf-8"},
          },
        )

        expect(res.status).toBe(200)
        expect(res.headers.get(HEADER_CSRF)).not.toBe(undefined)

        const cookieHeader = res.headers.get("Set-Cookie")
        const cookie = (name) => readCookie(cookieHeader, name)
        expect(cookieHeader).not.toBe(undefined)

        const publicDataStr = fromBase64(cookie(COOKIE_PUBLIC_DATA_TOKEN))
        const publicData = JSON.parse(publicDataStr)
        expect(publicData.userId).toBe(1)

        expect(readCookie(cookieHeader, COOKIE_SESSION_TOKEN)).not.toBe(undefined)
      })

      it("does not require CSRF header on HEAD requests", async () => {
        const res = await fetch(`http://localhost:${appPort}/api/noauth`, {
          method: "POST",
          headers: {"Content-Type": "application/json; charset=utf-8"},
        })
        const cookieHeader = res.headers.get("Set-Cookie")

        const headRes = await fetch(`http://localhost:${appPort}/api/noauth`, {
          method: "HEAD",
          headers: {
            "Content-Type": "application/json; charset=utf-8",
            cookie: cookieHeader as string,
          },
        })

        expect(headRes.status).toBe(200)
      })
    })
  })
}

describe("Auth Tests", () => {
  describe("dev mode - webpack", async () => {
    beforeAll(async () => {
      try {
        await runBlitzCommand(["prisma", "migrate", "reset", "--force"])
        appPort = await findPort()
        app = await blitzLaunchApp(appPort, {cwd: process.cwd()})
      } catch (error) {
        console.log(error)
      }
    }, 5000 * 60 * 2)
    afterAll(async () => {
      await killApp(app)
    })
    runTests()
  })

  describe("dev mode - turbo", async () => {
    beforeAll(async () => {
      try {
        await runBlitzCommand(["prisma", "migrate", "reset", "--force"])
        appPort = await findPort()
        app = await blitzLaunchApp(appPort, {cwd: process.cwd()}, true)
      } catch (error) {
        console.log(error)
      }
    }, 5000 * 60 * 2)
    afterAll(async () => {
      await killApp(app)
    })
    runTests()
  })
})
